#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
Tablet is a partition of Table


'''

__author__ = 'kuerant < kuerant@gmail.com >'
__version__ = '1.0.0'


import os, os.path
import md5
import types

import log

from error import TabletRangeError

from key import KeySmith
from key import ROW_KEY_MIN,ROW_KEY_MAX

from help import quote,unquote
from help import insort, bisectLeft
from const import PROPERTIES_FILENAME, CELL_DIR_NAME_PREFIX
from const import ROW_FOUND, COLUMN_FOUND, COLUMN_DELETED
from const import OP_DONE, OP_STOP

from cell import Cell

#######################################################################

class   TabletMutator(object) :
    '''TabletMutator manipulate records in Tablet, such as INSERT,
    UPDATE, ERASE, etc.
    '''
    def __init__(self, name, dir, min_key=ROW_KEY_MIN, max_key=ROW_KEY_MAX) :
        self.name = name        # tablet ID, should be UNIQUE ?
        self.dir = dir
        self.min_key = min_key
        self.max_key = max_key
        if self.min_key > self.max_key :
            self.min_key,self.max_key = self.max_key,self.min_key

        # cells collection, fill with tuple (key, (min_key, max_key, cell))
        self._cells = []

        # __init__()

    ###################################################################
    #
    # PUBLIC METHODS
    #

    def insert(self, row_key, columns, timestamp=None, unique_key=False) :
        '''insert column (and data) into tablet

        if any column already exists, insert operation fail.

        @param row_key row-key of record
        @param columns columns data of record. 
                       columns is iterable for tuple (family, qualifier, data)
        @param timestamp what time should the record be marked as
        @param unique_key ???

        @return (OP_DONE, None) -- insertion done
                (OP_STOP, columns_exist) -- insertion stopped on column existence
                (OP_FAIL, columns_fail)  -- insertion failed on some columns

        @exceptions KeyExistError,EmptyRowKeyError,TabletRangeError
        '''
        if not row_key : raise EmptyRowKeyError
        cell = self.__getCell(row_key)

        # make sure unique keys
        if unique_key :
            raise NotImplementedError('TODO: guarantee UNIQUE key.')

        smith = KeySmith()

        # check status of column
        columns_exist = []
        for family,qualifier,data in columns :
            if not family : family = ''
            if not qualifier : qualifier = ''
            column_key = smith.getColumnKey(row_key, family, qualifier)

            _status,_timestamp = cell.getColumnStatus(row_key, column_key)
            if COLUMN_FOUND == _status :
                columns_exist.append( (family, qualifier) )
        if columns_exist :
            return (OP_STOP, columns_exist)

        return self.__insert(cell, row_key, columns, timestamp)
        ## insert()

    def __insert(self, cell, row_key, columns, timestamp) :
        smith = KeySmith()
        if timestamp is None : timestamp = smith.getCurrentTimestamp()

        # insert record into Cell
        for family,qualifier,data in columns :
            if not family :
                raise ColumnFamilyError, 'Empty column family for key %s' % `row_key`
            if not data :
                raise ValueError, 'Empty Data for key %s' % `row_key`
            if not qualifier : qualifier = ''

            key = smith.assemble(row_key, family, qualifier, KeySmith.FLAG_INSERT, timestamp)
            if not isinstance(data, types.StringType) : data = str(data)
            cell.insert(row_key, key, data)

        return (OP_DONE, None)
        ## __insert()

    def update(self, row_key, columns, timestamp=None) :
        '''update column (and data) into tablet

        if any column does not exists, updation fail.

        @param row_key row-key of record
        @param columns columns data of record. 
                       columns is iterable for tuple (family, qualifier, data)
        @param timestamp what time should the record be marked as

        @return (OP_DONE, None) -- updation done
                (OP_STOP, columns_nonexist) -- updation stopped on column non-existence
                (OP_FAIL, columns_fail)  -- updation failed on some columns

        @exceptions EmptyRowKeyError,TabletRangeError
        '''
        if not row_key : raise EmptyRowKeyError
        cell = self.__getCell(row_key)

        smith = KeySmith()

        # check status of column
        columns_nonexist = []
        for family,qualifier,data in columns :
            if not family : family = ''
            if not qualifier : qualifier = ''
            column_key = smith.getColumnKey(row_key, family, qualifier)

            _status,_timestamp = cell.getColumnStatus(row_key, column_key)
            if COLUMN_FOUND != _status :
                columns_nonexist.append( (family, qualifier) )
        if columns_nonexist :
            return (OP_STOP, columns_nonexist)

        return self.__insert(cell, row_key, columns, timestamp)
        ## update()

    def upsert(self, row_key, columns, timestamp=None) :
        '''insert or update column (and data) into tablet 

        if any column does not exist, insert it.
        if any column do exist, update it.

        @param row_key row-key of record
        @param columns columns data of record. 
                       columns is iterable for tuple (family, qualifier, data)
        @param timestamp what time should the record be marked as

        @return (OP_DONE, None) -- updation done
                (OP_FAIL, columns_fail)  -- updation failed on some columns

        @exceptions EmptyRowKeyError,TabletRangeError
        '''
        if not row_key : raise EmptyRowKeyError
        cell = self.__getCell(row_key)

        return self.__insert(cell, row_key, columns, timestamp)
        ## upsert()

    def erase(self, row_key, columns, timestamp=None) :
        '''erase data from tablet by row-key

        @param row_key row-key of record
        @param columns columns data of record. 
                       columns is iterable for tuple (family, qualifier, data)
                       here, IGNORing data field

        @return columns_status, showing status of erasion on earch column
                        list of 3-tuple (family, qualifier, status)
                status are one of CELL_DELETED, CELL_NOT_FOUND, 
                                  COLUMN_DELETED, COLUMN_NOT_FOUND,
                                  ROW_DELETED, ROW_NOT_FOUND
                status are one of CELL_FOUND,COLUMN_FOUND,ROW_FOUND,
                because some of record survived timestamp
        @exception KeyNotFoundError
        '''
        cell = self.__getCell(row_key)

        return self.__erase(cell, row_key, columns, timestamp)
        ## erase()

    def __erase(self, cell, row_key, columns, timestamp) :
        if columns is None :
            # to delete ROW
            # maybe default columns should be '\xFF\xFF\xFF\xFF' to be sort first
            raise NotImplementedError('TODO: "columns=None" will be support later.')

        smith = KeySmith()
        if timestamp is None : timestamp = smith.getCurrentTimestamp()

        column_status = []
        for family,qualifier,data in columns :
            if not family : family = ''
            if not qualifier : qualifier = ''
            column_key = smith.getColumnKey(row_key, family, qualifier)

            # check status of column
            _status,_timestamp = cell.getColumnStatus(row_key, column_key)
            if COLUMN_FOUND == _status :
                if _timestamp < timestamp :
                    # found column, and do erase
                    key = smith.assemble(row_key, family, qualifier, KeySmith.FLAG_DELETE_COLUMN_FAMILY, timestamp)
                    data = '@%s' % `timestamp`
                    cell.insert(row_key, key, data)

                    _status = COLUMN_DELETED
            column_status.append( (family, qualifier, _status) )

        return column_status
        ## __erase()

    def findColumn(self, row_key, columns) :
        '''lookup column from tablet by row-key

        @param row_key row-key of record
        @param columns columns data of record. 
                       columns is iterable for tuple (family, qualifier, data)
                       here, IGNORing data field

        @return (columns_found, columns_not_found)
        columns_found : lists of 4-tuple (family,qualifier,data, timestamp)
        columns_not_found : lists of 4-tuple (family, qualifier, None, None)
        '''
        cell = self.__getCell(row_key)

        return self.__findColumn(cell, row_key, columns)
        ## findOne()

    def __findColumn(self, cell, row_key, columns) :
        if columns is None :
            raise NotImplementedError('TODO: "columns=None" will be support later.')

        columns_found = []
        columns_not_found = []

        smith = KeySmith()
        for family,qualifier,data in columns :
            if not family : family = ''
            if not qualifier : qualifier = ''
            column_key = smith.getColumnKey(row_key, family, qualifier)
            _status,_data,_timestamp = cell.findColumn(row_key, column_key)
            if COLUMN_FOUND == _status :
                columns_found.append( (family, qualifier, _data, _timestamp) )
            else :
                columns_not_found.append( (family, qualifier, None, None) )

        return (columns_found, columns_not_found)
        ## __findOne()

    ###################################################################
    #
    # PRIVATE METHODS
    #

    ## this method move to class KeySmith 
    #def __getCurrentTimestamp(self) :
    #    timestamp = int(time.time())
    #    return timestamp
    #    # __getCurrentTimestamp()

    def __getCell(self, row_key) :
        '''select cell by row-key

        if no cell is responsible for the row-key, raise TabletRangeError,
        meaning that the record should NOT be handled here.
        '''
        cell = None

        if not self.__isInCharge(row_key) :  
            raise TabletRangeError, 'Tablet "%s" (%s .. %s] do not cover row-key %s.' % (self.name, `self.min_key`, `self.max_key`, `row_key`)

        if not self._cells : self.__createCell(self.min_key, self.max_key)
        index = bisectLeft(self._cells, (row_key, None))
        _key,_cell_spec = self._cells[index]
        _min_key, _max_key, _cell = _cell_spec
        if not _min_key < row_key <= _max_key :
            raise RuntimeError, 'Cell (%s .. %s] do NOT cover row-key %s.' % (_min_key, _max_key, row_key)
        cell = _cell

        if not cell : raise RuntimeError, 'Tablet "%s" (%s .. %s] cover row-key %s ???' % (self.name, `self.min_key`, `self.max_key`, `row_key`)
        return cell
        # __getCell()

    def __isInCharge(self, row_key) :
        '''test if row_key belongs to (self.min_key, self.max_key]
        '''
        return self.min_key < row_key <= self.max_key
        ## __isInCharge()

    def __createCell(self, min_key, max_key) :
        suffix = md5.new(max_key).hexdigest()
        cell_dir_name = '%s%s' % (CELL_DIR_NAME_PREFIX, suffix)
        cell_dir_path = os.sep.join((self.dir, cell_dir_name))

        cell = Cell(cell_dir_path)
        insort(self._cells, (min_key, (min_key, max_key, cell)))
        ## __createCell()

    # class TabletMutator


#######################################################################

class   TabletScanner(object) :
    '''TabletScanner query record in Tablet.
    '''
    def __init__(self) :
        self.size = 0   # size of data in cache
        # __init__()

    ###################################################################
    ##
    ## PUBLIC METHODS
    ##

    def findOne(self, row_key, columns, timestamp=None) :
        raise NotImplementedError()
        # findOne()

    def scan(self, row_key, columns, timestamp=None) :
        raise NotImplementedError()
        # scan()

    ###################################################################
    ##
    ## PRIVATE METHODS
    ##

    # class TabletMutator

